#include <windows.h>
#include <BaseTsd.h>

#include <iostream>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
#include <chrono>
#include <ctime>
#include <clocale>
#include <atomic>

#include "internal/FakeRoot.h"
#include "internal/LocalFS.h"

#include "log.h"
#include "FuseServer.h"
#include "VioSAPI.h"
#include "api/gfs.h"

class FuseServer : public IFSL {
public:
    FuseServer();
    ~FuseServer();

    bool StartServer(int portNum);
    void Release();

    bool AddFileList(
        const std::string& shareName,
        const FileShareList& shareList,
        uint64_t addTimeStamp, bool hidden, bool readOnly);
    int RemoveShare(const std::string& shareName, bool force);

    bool GetLastAccessTime(const std::string& shareName, uint64_t& timeStamp);
    HANDLE GetTransportHandle();
    void SetPeerReconnect(bool isReconnect);

private:
    PathMapper m_pathMap;
    std::mutex m_pathMapGuard;
    std::thread m_serverLoop;
    std::atomic<ConnectionStatus> m_peerConnectionStatus;
    std::atomic<bool> m_serverExit;
    VioSAPI* m_transport;
    LocalFS* m_localFS;
    bool m_fake;
    bool m_root;
    bool m_bInitialized;

    VioSOpStatus SrvReqRecv(GuestFuseSrvReq* req);
    VioSOpStatus SrvRplSend(GuestFuseSrvRPL* rpl);
    void SrvReqProcess(GuestFuseSrvReq* req, GuestFuseSrvRPL* rpl);
    void ServerLoop();
    int SrvReqProcessFake(GuestFuseSrvReq* req, GuestFuseSrvRPL* rpl, std::string& shareName);
    int SrvReqProcessReal(GuestFuseSrvReq* req, GuestFuseSrvRPL* rpl, std::string& shareName,
                          std::string& fullPath);
    void RemoveAllFileHandle();
};

FuseServer::FuseServer()
    : m_pathMap(), m_serverExit(false),
      m_peerConnectionStatus(ConnectionStatus::DISCONNECTED),
      m_fake(false), m_root(false), m_bInitialized(false)
{
    m_pathMap.insert({ALIAS_CUR, {}});
    m_pathMap.insert({ALIAS_PREV, {}});

    m_transport = new VioSAPI();
    m_localFS = new LocalFS();
}

FuseServer::~FuseServer()
{
    if (!m_serverExit) {
        m_serverExit = true;
    }

    if (m_serverLoop.joinable()) {
        m_serverLoop.join();
    }

    if (m_transport) {
        ConnectionStatus expected = ConnectionStatus::CONNECTED;
        if (m_peerConnectionStatus.compare_exchange_strong(expected, ConnectionStatus::DISCONNECTED)) {
            m_transport->Disconnect();
        }
        delete m_transport;
        m_transport = NULL;
    }

    if (m_localFS) {
        delete m_localFS;
        m_localFS = NULL;
    }
}

HANDLE FuseServer::GetTransportHandle()
{
    if (m_transport) {
        return m_transport->GetHandle();
    }

    return INVALID_HANDLE_VALUE;
}

void FuseServer::SetPeerReconnect(bool isReconnect)
{
    if (isReconnect) {
        ConnectionStatus expected = ConnectionStatus::CONNECTED;
        m_peerConnectionStatus.compare_exchange_strong(expected, ConnectionStatus::DISCONNECTED);
    } else {
        ConnectionStatus expected = ConnectionStatus::DISCONNECTED;
        m_peerConnectionStatus.compare_exchange_strong(expected, ConnectionStatus::CONNECTED);
    }
}

VioSOpStatus FuseServer::SrvReqRecv(GuestFuseSrvReq* req)
{
    VioSOpStatus res;

    res = m_transport->RecvPacket(req, REQ_BUFF_MAX, GFS_RECVPKT_IFACE_DELAY);
    if (res != VioSOpStatus::SUCCESS) {
        return res;
    }

    if (req->hdr.magic != REQ_HDR_MAGIC) {
        LOGE("Protocol error: hdr magic {}", req->hdr.magic);
        return VioSOpStatus::BROKEN_PROTOCOL;
    }
    if (GFS_REQ_OP_INVALID(req->hdr.opcode)) {
        LOGE("Protocol error: opcode {}", req->hdr.opcode);
        return VioSOpStatus::BROKEN_PROTOCOL;
    }
    if (!req->hdr.len) {
        LOGE("Protocol error: zero request length.");
        return VioSOpStatus::BROKEN_PROTOCOL;
    }
    return VioSOpStatus::SUCCESS;
}

VioSOpStatus FuseServer::SrvRplSend(GuestFuseSrvRPL* rpl)
{
    VioSOpStatus res;
    uint32_t rplLen = rpl->hdr.len + RPL_HDR_SZ;

    res = m_transport->SendPacket(rpl, rplLen, GFS_SENDPKT_TOTAL_DELAY);
    if (res != VioSOpStatus::SUCCESS) {
        LOGE("Failed to send reply error: {}", (int)res);
    }

    return res;
}

int FuseServer::SrvReqProcessFake(GuestFuseSrvReq* req, GuestFuseSrvRPL* rpl, std::string& shareName)
{
    int ret = RETURN_ERROR;

    auto shareContentIter = m_pathMap.find(shareName);
    if (shareContentIter != m_pathMap.end()) {
        shareContentIter->second.lastAccessTime = std::chrono::seconds(std::time(nullptr)).count();
    }

    switch (req->hdr.opcode) {
        case GFS_REQ_ATTR:
            ret = FakeRoot::GetAttr(rpl);
            break;
        case GFS_REQ_RDDIR: {
            std::vector<std::pair<const std::string&, bool>> fakeContent;

            if (!m_root) {
                const auto& const_share_st = shareContentIter->second;
                for (const auto& [fake_name, _] : const_share_st.fileMap) {
                    fakeContent.push_back({fake_name, false});
                }
            } else {
                for (const auto& [fake_name, map_st] : m_pathMap) {
                    fakeContent.push_back({fake_name, map_st.hidden});
                }
            }

            ret = FakeRoot::ReadDir(fakeContent, rpl, RPL_BUFF_MAX);
            break;
        }
        case GFS_REQ_OPEN:
            ret = FakeRoot::Open(rpl);
            break;
        default:
            LOGE("Unknown request opcode {}", req->hdr.opcode);
    };

    return ret;
}

#define READONLY_CHECK(cond) \
    if (cond) return (-EACCES)

int FuseServer::SrvReqProcessReal(GuestFuseSrvReq* req, GuestFuseSrvRPL* rpl, std::string& shareName,
                                  std::string& fullPath)
{
    auto shareContentIter = m_pathMap.find(shareName);
    if (shareContentIter != m_pathMap.end()) {
        shareContentIter->second.lastAccessTime = std::chrono::seconds(std::time(nullptr)).count();
    }

    switch (req->hdr.opcode) {
        case GFS_REQ_ATTR:
            return m_localFS->GetAttr(fullPath.c_str(), rpl);
        case GFS_REQ_OPEN:
            return m_localFS->Open(fullPath.c_str(), req->md, rpl,
                                   shareContentIter->second.readOnly, shareContentIter->first);
        case GFS_REQ_RELEASE:
            return m_localFS->Release(fullPath.c_str(), req->md, rpl, shareContentIter->first);
        case GFS_REQ_READ:
            return m_localFS->Read(fullPath.c_str(), req->md, rpl, shareContentIter->first);
        case GFS_REQ_RDDIR:
            return m_localFS->ReadDir(fullPath.c_str(), rpl, RPL_BUFF_MAX);
        case GFS_REQ_WRITE:
            READONLY_CHECK(shareContentIter->second.readOnly);
            return m_localFS->Write(fullPath.c_str(), req->md, &req->data[strlen(req->data) + 1],
                                    rpl, shareContentIter->first);
        case GFS_REQ_RENAME: {
            READONLY_CHECK(shareContentIter->second.readOnly);
            std::string newShareName;
            std::string newFullPath;
            bool success = FakeRoot::TranslatePath(m_pathMap, &req->data[strlen(req->data) + 1],
                                                   newShareName, newFullPath, m_root, m_fake);
            READONLY_CHECK((!success || m_fake));
            return m_localFS->Rename(fullPath.c_str(), newFullPath.c_str());
        }
        case GFS_REQ_REMOVE:
            READONLY_CHECK(shareContentIter->second.readOnly);
            return m_localFS->Remove(fullPath.c_str());
        case GFS_REQ_TRUNCATE:
            READONLY_CHECK(shareContentIter->second.readOnly);
            return m_localFS->Truncate(fullPath.c_str(), req->md);
        case GFS_REQ_MKDIR:
            READONLY_CHECK(shareContentIter->second.readOnly);
            return m_localFS->MkDir(fullPath.c_str());
        case GFS_REQ_RMDIR:
            READONLY_CHECK(shareContentIter->second.readOnly);
            return m_localFS->RmDir(fullPath.c_str());
        default:
            return RETURN_ERROR;
    };
}

#undef READONLY_CHECK

void FuseServer::SrvReqProcess(GuestFuseSrvReq* req, GuestFuseSrvRPL* rpl)
{
    std::string shareName;
    std::string fullPath;
    bool success;

    rpl->hdr.magic = RPL_HDR_MAGIC;
    rpl->hdr.opcode = req->hdr.opcode;
    rpl->hdr.opid = req->hdr.opid;
    rpl->hdr.len = 0;

    std::lock_guard<std::mutex> guard(m_pathMapGuard);
    success = FakeRoot::TranslatePath(m_pathMap, req->data, shareName, fullPath, m_root, m_fake);
    if (!success) {
        LOGE("Wrong request path");
        rpl->hdr.opret = -ENOENT;
        return;
    }

    int ret = RETURN_ERROR;
    if (m_fake) {
        ret = SrvReqProcessFake(req, rpl, shareName);
    } else {
        ret = SrvReqProcessReal(req, rpl, shareName, fullPath);
    }

    rpl->hdr.opret = ret;

    return;
}

void FuseServer::ServerLoop()
{
    GuestFuseSrvReq* req = (GuestFuseSrvReq*)LocalAlloc(LMEM_FIXED, REQ_BUFF_MAX);
    if (req == nullptr) {
        LOGE("REQ LocalAlloc failed with error: {}", GetLastError());
        return;
    }
    GuestFuseSrvRPL* rpl = (GuestFuseSrvRPL*)LocalAlloc(LMEM_FIXED, RPL_BUFF_MAX);
    if (rpl == nullptr) {
        LOGE("RPL LocalAlloc failed with error: {}", GetLastError());
        LocalFree(req);
        return;
    }

    VioSOpStatus res;
    m_peerConnectionStatus.store(ConnectionStatus::DISCONNECTED);

    while (!m_serverExit) {
        ConnectionStatus expected = ConnectionStatus::DISCONNECTED;
        while (m_peerConnectionStatus.compare_exchange_strong(expected, ConnectionStatus::CONNECTING)) {
            Sleep(VIOS_CONN_TIMEOUT);
            RemoveAllFileHandle();
            res = m_transport->Connect(1);
            if (res == VioSOpStatus::BROKEN_CONN) {
                m_serverExit = true;
            } else if (res == VioSOpStatus::SUCCESS) {
                LOGI("Fuse client connected");
                m_peerConnectionStatus.store(ConnectionStatus::CONNECTED);
                break;
            }
            if (m_serverExit) {
                m_peerConnectionStatus.store(ConnectionStatus::DISCONNECTED);
                break;
            }
        }

        if (m_peerConnectionStatus.load() == ConnectionStatus::DISCONNECTED) {
            m_serverExit = true;
            break;
        }

        VioSOpStatus res = SrvReqRecv(req);
        if (res != VioSOpStatus::SUCCESS) {
            if (res == VioSOpStatus::TIMEOUT) {
                continue;
            }

            LOGE("Request read error: {}", (int)res);
            if (res == VioSOpStatus::BROKEN_CONN) {
                m_peerConnectionStatus.store(ConnectionStatus::DISCONNECTED);
                m_serverExit = true;
            } else if (res == VioSOpStatus::BROKEN_PROTOCOL) {
                m_transport->PurgeIO();
                m_peerConnectionStatus.store(ConnectionStatus::DISCONNECTED);
                continue;
            }
        }

        SrvReqProcess(req, rpl);
        if (req->hdr.opcode == GFS_REQ_READ) {
            continue;
        }

        res = SrvRplSend(rpl);
        if (res != VioSOpStatus::SUCCESS) {
            m_peerConnectionStatus.store(ConnectionStatus::DISCONNECTED);
        }
    }

    LocalFree(req);
    LocalFree(rpl);
}

bool FuseServer::StartServer(int portNum)
{
    if (m_bInitialized) {
        LOGW("Already start fuse server!");
        return true;
    }

    bool res = m_localFS->Initialize();
    if (!res) {
        LOGE("Failed to initialise localFS");
        return false;
    }

    res = m_transport->InitDev(portNum);
    if (!res) {
        LOGE("Can't get virtio serial device port {}", portNum);
        return false;
    }

    m_serverLoop = std::thread::thread(&FuseServer::ServerLoop, this);
    m_bInitialized = true;

    return true;
}

void FuseServer::Release()
{
    delete this;
}

bool FuseServer::AddFileList(
    const std::string& shareName,
    const FileShareList& shareList,
    uint64_t addTimeStamp, bool hidden, bool readOnly)
{
    std::lock_guard<std::mutex> guard(m_pathMapGuard);

    if (m_pathMap.find(shareName) != m_pathMap.end()) {
        LOGE("Cannot file list with share name {}, name is already in use.", shareName.c_str());
        return false;
    }

    auto [pathMapIter, _] =
        m_pathMap.insert({shareName, {{}, addTimeStamp, true, hidden, readOnly}});

    pathMapIter->second.fileMap.insert(shareList.begin(), shareList.end());

    return true;
}

int FuseServer::RemoveShare(const std::string& shareName, bool force)
{
    std::lock_guard<std::mutex> guard(m_pathMapGuard);

    auto pathMapIter = m_pathMap.find(shareName);
    if (pathMapIter == m_pathMap.end()) {
        LOGE("Cannot remove share name {}, name doesn't exist.", shareName.c_str());
        return ERROR_REMOVE_SHARE_NOT_FOUND;
    }

    auto shareContent = pathMapIter->second;
    if (!force && shareContent.fake && shareContent.hidden &&
        m_localFS->CheckShareInUse(shareName)) {
        return ERROR_REMOVE_SHARE_IN_USE;
    }

    m_localFS->ShareCleanup(shareName);
    m_pathMap.erase(pathMapIter);

    return REMOVE_SHARE_SUCCESS;
}

void FuseServer::RemoveAllFileHandle()
{
    std::lock_guard<std::mutex> guard(m_pathMapGuard);
    m_localFS->RemoveAllFileHandle();
}

bool FuseServer::GetLastAccessTime(const std::string& shareName, uint64_t& timestamp)
{
    std::lock_guard<std::mutex> guard(m_pathMapGuard);

    auto pathMapIter = m_pathMap.find(shareName);
    if (pathMapIter == m_pathMap.end()) {
        LOGE("Cannot get access time of share name {}, name doesn't exist.", shareName.c_str());
        return false;
    }

    timestamp = pathMapIter->second.lastAccessTime;
    return true;
}

#if defined(FSL_STATIC)
#define FSLAPI
#define APIENTSPEC
extern "C" FSLAPI IFSL* APIENTSPEC GetFSL()
{
    return new FuseServer();
}
#endif
